package com.cus.tom.transfer.trans.service;

import static com.cus.tom.transfer.trans.io.DirectoryReader.LOG_TAG;

import android.app.Notification;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;

import com.cus.tom.transfer.App;
import com.cus.tom.transfer.R;
import com.cus.tom.transfer.bean.Const;
import com.cus.tom.transfer.bean.FileType;
import com.cus.tom.transfer.bean.TypeFile;
import com.cus.tom.transfer.frag.ImgFragment;
import com.cus.tom.transfer.trans.io.AverageRateCounter;
import com.cus.tom.transfer.trans.io.BufferPool;
import com.cus.tom.transfer.trans.io.Channel;
import com.cus.tom.transfer.trans.io.DirectoryReader;
import com.cus.tom.transfer.utils.Utils;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;

public class SendService extends TransferService {
    private List<TypeFile> files = new LinkedList<>();

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if ("cancel".equals(intent.getAction())) {
            Log.d(LOG_TAG, "SendService user cancelled");
            stop();
            return START_NOT_STICKY;
        }

        this.startId = startId;
        initNotification(R.string.notification_wait_new);

        List<String> tmp = intent.getStringArrayListExtra("files");
        if (tmp != null) {
            for (String s : tmp) {
                files.add(new TypeFile(s, FileType.APP));
            }
        }
        HashSet<Integer> set = (HashSet) intent.getSerializableExtra("set");
        if (set.contains(FileType.PIC.getCode())) {
            for (String s : ImgFragment.img) {
                files.add(new TypeFile(s, FileType.PIC));
            }
        }
        if (set.contains(FileType.VIDEO.getCode())) {
            for (String s : ImgFragment.video) {
                files.add(new TypeFile(s, FileType.VIDEO));
            }
        }
        if (set.contains(FileType.AUDIO.getCode())) {
            for (String s : ImgFragment.audio) {
                files.add(new TypeFile(s, FileType.AUDIO));
            }
        }
        if (set.contains(FileType.CONTRACT.getCode())) {
            File f = new File(Const.CONTRACT_PATH);
            if (f.exists() && f.length() > 0) {
                files.add(new TypeFile(f.getAbsolutePath(), FileType.CONTRACT));
            }
        }
        if (set.contains(FileType.SMS.getCode())) {
            File f = new File(Const.SMS_PATH);
            if (f.exists() && f.length() > 0) {
                files.add(new TypeFile(f.getAbsolutePath(), FileType.SMS));
            }
        }
        if (set.contains(FileType.CALL.getCode())) {
            File f = new File(Const.CALL_PATH);
            if (f.exists() && f.length() > 0) {
                files.add(new TypeFile(f.getAbsolutePath(), FileType.CALL));
            }
        }
        if (files == null || files.isEmpty()) {
            stopSelf();
            return START_NOT_STICKY;
        }
        acquireLocks();
        thread = new TransferThread();
        thread.start();
        return START_NOT_STICKY;
    }

    @Override
    public void onCreate() {
        ((App) getApplicationContext()).sendService = this;
        postUpdateButton();
    }

    @Override
    public void onDestroy() {
        showResult();
        ((App) getApplicationContext()).sendService = null;
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException();
    }

    private class TransferThread extends Thread {
        private final BufferPool bufferPool = new BufferPool(BufferSize);
        Socket socket = null;
        ServerSocket listener = null;

        @Override
        public void interrupt() {
            {
                final ServerSocket listener = this.listener;
                if (listener != null) {
                    try {
                        listener.close();
                    } catch (IOException ignored) {
                    }
                }
            }
            {
                final Socket socket = this.socket;
                if (socket != null) {
                    try {
                        socket.setSoLinger(true, 0);
                        socket.close();
                    } catch (IOException ignored) {
                    }
                }
            }
            super.interrupt();
        }

        @Override
        public void run() {
            try {
                try {
                    while (!isInterrupted()) {
                        listener = new ServerSocket(Const.TCP_PORT);
                        Log.d(LOG_TAG, "ReceiveService begins to listen");
                        listener.setSoTimeout(0); // prevent thread leak
                        listener.setPerformancePreferences(0, 0, 1);
                        listener.setReceiveBufferSize(TcpBufferSize);
                        while (!isInterrupted()) {
                            try {
                                socket = listener.accept();
                                break;
                            } catch (BindException e) {
                                ++Const.TCP_PORT;
                                break;
                            } catch (SocketTimeoutException e) {
                                e.printStackTrace();
                            }
                        }
                        if (socket != null) {
                            break;
                        }
                    }
                } finally {
                    listener.close();
                    listener = null;
                }
                if (socket == null) {
                    return;
                }
                handler.post(() -> {
                    if (builder != null && notificationManager != null) {
                        builder.setContentTitle(getString(R.string.notification_sending));
                        notificationManager.notify(startId, builder.build());
                    }
                    sendBroadcast(new Intent(Const.SEND_START));
                });
                try {
                    Log.d(LOG_TAG, "ReceiveService accepted connection");
                    socket.setPerformancePreferences(0, 0, 1);
                    socket.setTrafficClass(Const.IPTOS_THROUGHPUT);
                    socket.setReceiveBufferSize(TcpBufferSize);
                    socket.setSendBufferSize(TcpBufferSize);
                    socket.setSoTimeout(30000);
                    streamCopy(socket);
                } catch (IOException e) {
                    Log.e(LOG_TAG, "pipe", e);
                } finally {
                    if (socket != null) {
                        try {
                            socket.close();
                        } catch (IOException e) {
                            Log.e(LOG_TAG, "socket close failed", e);
                        }
                        socket = null;
                    }
                }
            } catch (Exception e) {
                Log.e(LOG_TAG, "ReceiveService unexpected exception", e);
            } finally {
                Log.d(LOG_TAG, "ReceiveService closing");
                handler.post(SendService.this::stop);
            }
        }

        private void streamCopy(Socket socket) {
            final Channel channel = new Channel(8 * 1024 * 1024); // 8MB
            final Progress progress = new Progress();
            final DirectoryReader reader = new DirectoryReader(getContentResolver(), files,
                    channel, progress, bufferPool);
            reader.start();
            Timer timer = new Timer();
            try (OutputStream out = socket.getOutputStream()) {
                AverageRateCounter rate = new AverageRateCounter(5);
                timer.schedule(new TimerTask() {

                    @Override
                    public void run() {
                        final Progress p = progress.get();
                        String text = p.text;
                        if (text != null) {
                            text += "\n";
                            final int max = channel.getCapacity();
                            final int used = max - channel.getAvailable();
                            text += String.format(
                                    Locale.getDefault(),
                                    getResources().getString(R.string.buffer_indicator),
                                    Utils.formatSize(used),
                                    Utils.formatSize(max));
                        } else {
                            text = getResources().getString(R.string.notification_finishing);
                        }
                        final String contentText = text;
                        final boolean indeterminate = p.max == 0;
                        final int max, now;
                        if (indeterminate) {
                            max = 0;
                            now = 0;
                        } else {
                            max = 1000;
                            now = (int) (p.now * 1000 / p.max);
                        }
                        handler.post(() -> {
                            if (builder != null && notificationManager != null) {
                                builder.setContentText(contentText)
                                        .setStyle(new Notification.BigTextStyle().bigText(contentText))
                                        .setProgress(max, now, indeterminate)
                                        .setSubText(Utils.formatSize(rate.rate()) + "/s");
                                notificationManager.notify(startId, builder.build());
                            }
                            Intent i = new Intent(Const.SEND_SPEED);
                            i.putExtra("rate", rate.rate());
                            i.putExtra("p", p);
                            sendBroadcast(i);
                        });
                    }
                }, 1000, 1000);
                while (true) {
                    final ByteBuffer packet = channel.read();
                    if (packet == null) {
                        break;
                    }
                    rate.increase(packet.limit());
                    out.write(packet.array(), packet.arrayOffset() + packet.position(), packet.remaining());
                    bufferPool.push(packet);
                }
                out.flush();
                result = reader.isSuccess();
                reader.join();
                Log.d(LOG_TAG, "SendService finished normally");
            } catch (InterruptedException e) {
                Log.d(LOG_TAG, "SendService interrupted");
            } catch (IOException e) {
                Log.e(LOG_TAG, "SendService", e);
            } finally {
                handler.post(() -> {
                    Intent i = new Intent(Const.SEND_OVER);
                    i.putExtra("result", result);
                    i.putExtra("id", reader.getFlag());
                    final Progress p = progress.get();
                    i.putExtra("p", p);
                    sendBroadcast(i);
                });
                handler.postDelayed(() -> {
                    timer.cancel();
                    reader.interrupt();
                }, 500);
            }
        }
    }
}
